https://postgrespro.ru/docs/postgrespro/12/textsearch

Глава 
# 12. Полнотекстовый поиск
Содержание

12.1. Введение
12.1.1. Что такое документ?
12.1.2. Простое соответствие текста
12.1.3. Конфигурации
12.2. Таблицы и индексы
12.2.1. Поиск в таблице
12.2.2. Создание индексов
12.3. Управление текстовым поиском
12.3.1. Разбор документов
12.3.2. Разбор запросов
12.3.3. Ранжирование результатов поиска
12.3.4. Выделение результатов
12.4. Дополнительные возможности
12.4.1. Обработка документов
12.4.2. Обработка запросов
12.4.3. Триггеры для автоматического обновления
12.4.4. Сбор статистики по документу
12.5. Анализаторы
12.6. Словари
12.6.1. Стоп-слова
12.6.2. Простой словарь
12.6.3. Словарь синонимов
12.6.4. Тезаурус
12.6.5. Словарь Ispell
12.6.6. Словарь Snowball
12.7. Пример конфигурации
12.8. Тестирование и отладка текстового поиска
12.8.1. Тестирование конфигурации
12.8.2. Тестирование анализатора
12.8.3. Тестирование словаря
12.9. Типы индексов GIN и GiST
12.10. Поддержка psql
12.11. Ограничения

12.1. Введение
12.1.1. Что такое документ?
12.1.2. Простое соответствие текста
12.1.3. Конфигурации
Полнотекстовый поиск (или просто поиск текста) — это возможность находить документы на естественном языке, соответствующие запросу, и, возможно, дополнительно сортировать их по релевантности для этого запроса. Наиболее распространённая задача — найти все документы, содержащие слова запроса, и выдать их отсортированными по степени соответствия запросу. Понятия запроса и соответствия довольно расплывчаты и зависят от конкретного приложения. В самом простом случае запросом считается набор слов, а соответствие определяется частотой слов в документе.

Операторы текстового поиска существуют в СУБД уже многие годы. В Postgres Pro для текстовых типов данных есть операторы ~, ~*, LIKE и ILIKE, но им не хватает очень важных вещей, которые требуются сегодня от информационных систем:

- Нет поддержки лингвистического функционала, даже для английского языка. Возможности регулярных выражений ограничены — они не рассчитаны на работу со словоформами, например, подходят и подходить. С ними вы можете пропустить документы, которые содержат подходят, но, вероятно, и они представляют интерес при поиске по ключевому слову подходить. Конечно, можно попытаться перечислить в регулярном выражении все варианты слова, но это будет очень трудоёмко и чревато ошибками (некоторые слова могут иметь десятки словоформ).

- Они не позволяют упорядочивать результаты поиска (по релевантности), а без этого поиск неэффективен, когда находятся сотни подходящих документов.

- Они обычно выполняются медленно из-за отсутствия индексов, так как при каждом поиске приходится просматривать все документы.

Полнотекстовая индексация заключается в предварительной обработке документов и сохранении индекса для последующего быстрого поиска. Предварительная обработка включает следующие операции:

#### Разбор документов на фрагменты.
 При этом полезно выделить различные классы фрагментов, например, числа, слова, словосочетания, почтовые адреса и т. д., которые будут обрабатываться по-разному. В принципе классы фрагментов могут зависеть от приложения, но для большинства применений вполне подойдёт предопределённый набор классов. Эту операцию в Postgres Pro выполняет анализатор (parser). Вы можете использовать как стандартный анализатор, так и создавать свои, узкоспециализированные.

#### Преобразование фрагментов в лексемы.
 Лексема — это нормализованный фрагмент, в котором разные словоформы приведены к одной. Например, при нормализации буквы верхнего регистра приводятся к нижнему, а из слов обычно убираются окончания (в частности, s или es в английском). Благодаря этому можно находить разные формы одного слова, не вводя вручную все возможные варианты. Кроме того, на данном шаге обычно исключаются стоп-слова, то есть слова, настолько распространённые, что искать их нет смысла. (Другими словами, фрагменты представляют собой просто подстроки текста документа, а лексемы — это слова, имеющие ценность для индексации и поиска.) Для выполнения этого шага в Postgres Pro используются словари. Набор существующих стандартных словарей при необходимости можно расширять, создавая свои собственные.

#### Хранение документов в форме, подготовленной для поиска. 
Например, каждый документ может быть представлен в виде сортированного массива нормализованных лексем. Помимо лексем часто желательно хранить информацию об их положении для ранжирования по близости, чтобы документ, в котором слова запроса расположены «плотнее», получал более высокий ранг, чем документ с разбросанными словами.

- Словари позволяют управлять нормализацией фрагментов с большой гибкостью. Создавая словари, можно:

- Определять стоп-слова, которые не будут индексироваться.

- Сопоставлять синонимы с одним словом, используя Ispell.

- Сопоставлять словосочетания с одним словом, используя тезаурус.

- Сопоставлять различные склонения слова с канонической формой, используя словарь Ispell.

- Сопоставлять различные склонения слова с канонической формой, используя стеммер Snowball.

Для хранения подготовленных документов в PostgreSQL предназначен тип данных tsvector, а для представления обработанных запросов — тип tsquery (Раздел 8.11). С этими типами данных работают целый ряд функций и операторов (Раздел 9.13), и наиболее важный из них — оператор соответствия @@, с которым мы познакомимся в Подразделе 12.1.2. Для ускорения полнотекстового поиска могут применяться индексы (Раздел 12.9).

### 12.1.1. Что такое документ?

Документ — это единица обработки в системе полнотекстового поиска; например, журнальная статья или почтовое сообщение. Система поиска текста должна уметь разбирать документы и сохранять связи лексем (ключевых слов) с содержащим их документом. Впоследствии эти связи могут использоваться для поиска документов с заданными ключевыми словами.

В контексте поиска в Postgres Pro документ — это обычно содержимое текстового поля в строке таблицы или, возможно, сочетание (объединение) таких полей, которые могут храниться в разных таблицах или формироваться динамически. Другими словами, документ для индексации может создаваться из нескольких частей и не храниться где-либо как единое целое. Например:

    SELECT title || ' ' ||  author || ' ' ||  abstract || ' ' || body
    AS document
    FROM messages
    WHERE mid = 12;

    SELECT m.title || ' ' || m.author || ' ' || m.abstract || ' ' || d.body
    AS document
    FROM messages m, docs d
    WHERE mid = did AND mid = 12;
> Примечание
На самом деле в этих примерах запросов следует использовать функцию coalesce, чтобы значение NULL в каком-либо одном атрибуте не привело к тому, что результирующим документом окажется NULL.

Документы также можно хранить в обычных текстовых файлах в файловой системе. В этом случае база данных может быть просто хранилищем полнотекстового индекса и исполнителем запросов, а найденные документы будут загружаться из файловой системы по некоторым уникальным идентификаторам. Однако для загрузки внешних файлов требуются права суперпользователя или поддержка специальных функций, так что это обычно менее удобно, чем хранить все данные внутри БД Postgres Pro. Кроме того, когда всё хранится в базе данных, это упрощает доступ к метаданным документов при индексации и выводе результатов.

Для нужд текстового поиска каждый документ должен быть сведён к специальному формату tsvector. Поиск и ранжирование выполняется исключительно с этим представлением документа — исходный текст потребуется извлечь, только когда документ будет отобран для вывода пользователю. Поэтому мы часто подразумеваем под tsvector документ, тогда как этот тип, конечно, содержит только компактное представление всего документа.

### 12.1.2. Простое соответствие текста
Полнотекстовый поиск в Postgres Pro реализован на базе оператора соответствия @@, который возвращает true, если tsvector (документ) соответствует tsquery (запросу). Для этого оператора не важно, какой тип записан первым:

    SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@
    'cat & rat'::tsquery;
    ?column?
    ----------
    t

    SELECT 'fat & cow'::tsquery @@
    'a fat cat sat on a mat and ate a fat rat'::tsvector;
    ?column?
    ----------
    f
Как можно догадаться из этого примера, tsquery — это не просто текст, как и tsvector. Значение типа tsquery содержит искомые слова, это должны быть уже нормализованные лексемы, возможно объединённые в выражение операторами И, ИЛИ, НЕ и ПРЕДШЕСТВУЕТ. (Подробнее синтаксис описан в Подразделе 8.11.2.) Вы можете воспользоваться функциями to_tsquery, plainto_tsquery и phraseto_tsquery, которые могут преобразовать заданный пользователем текст в значение tsquery, прежде всего нормализуя слова в этом тексте. Функция to_tsvector подобным образом может разобрать и нормализовать текстовое содержимое документа. Так что запрос с поиском соответствия на практике выглядит скорее так:

    SELECT to_tsvector('fat cats ate fat rats') @@ to_tsquery('fat & rat');
    ?column? 
    ----------
    t
    Заметьте, что соответствие не будет обнаружено, если запрос записан как

    SELECT 'fat cats ate fat rats'::tsvector @@ to_tsquery('fat & rat');
    ?column? 
    ----------
    f
так как слово rats не будет нормализовано. Элементами tsvector являются лексемы, предположительно уже нормализованные, так что rats считается не соответствующим rat.

Оператор @@ также может принимать типы text, позволяя опустить явные преобразования текстовых строк в типы tsvector и tsquery в простых случаях. Всего есть четыре варианта этого оператора:

    tsvector @@ tsquery
    tsquery  @@ tsvector
    text @@ tsquery
    text @@ text
    Первые два мы уже видели раньше. Форма text@@tsquery равнозначна выражению to_tsvector(x) @@ y, а форма text@@text — выражению to_tsvector(x) @@ plainto_tsquery(y).

В значении tsquery оператор & (И) указывает, что оба его операнда должны присутствовать в документе, чтобы он удовлетворял запросу. Подобным образом, оператор | (ИЛИ) указывает, что в документе должен присутствовать минимум один из его операндов, тогда как оператор ! (НЕ) указывает, что его операнд не должен присутствовать, чтобы условие удовлетворялось. Например, запросу fat & ! rat соответствуют документы, содержащие fat и не содержащие rat.

Фразовый поиск возможен с использованием оператора <-> (ПРЕДШЕСТВУЕТ) типа tsquery, который находит соответствие, только если его операнды расположены рядом и в заданном порядке. Например:

    SELECT to_tsvector('fatal error') @@ to_tsquery('fatal <-> error');
    ?column? 
    ----------
    t

    SELECT to_tsvector('error is not fatal') @@ to_tsquery('fatal <-> error');
    ?column? 
    ----------
    f
Более общая версия оператора ПРЕДШЕСТВУЕТ имеет вид <N>, где N — целое число, выражающее разность между позициями найденных лексем. Запись <1> равнозначна <->, тогда как <2> допускает существование ровно одной лексемы между этими лексемами и т. д. Функция phraseto_tsquery задействует этот оператор для конструирования tsquery, который может содержать многословную фразу, включающую в себя стоп-слова. Например:

    SELECT phraseto_tsquery('cats ate rats');
        phraseto_tsquery        
    -------------------------------
    'cat' <-> 'ate' <-> 'rat'

    SELECT phraseto_tsquery('the cats ate the rats');
        phraseto_tsquery        
    -------------------------------
    'cat' <-> 'ate' <2> 'rat'
Особый случай, который иногда бывает полезен, представляет собой запись <0>, требующая, чтобы обоим лексемам соответствовало одно слово.

Сочетанием операторов tsquery можно управлять, применяя скобки. Без скобок операторы имеют следующие приоритеты, в порядке возрастания: |, &, <-> и самый приоритетный — !.

Стоит отметить, что операторы И/ИЛИ/НЕ имеют несколько другое значение, когда они применяются в аргументах оператора ПРЕДШЕСТВУЕТ, так как в этом случае имеет значение точная позиция совпадения. Например, обычному !x соответствуют только документы, не содержащие x нигде. Но условию !x <-> y соответствует y, если оно не следует непосредственно за x; при вхождении x в любом другом месте документа он не будет исключаться из рассмотрения. Другой пример: для условия x & y обычно требуется, чтобы и x, и y встречались в каком-то месте документа, но для выполнения условия (x & y) <-> z требуется, чтобы x и y располагались в одном месте, непосредственно перед z. Таким образом, этот запрос отличается от x <-> z & y <-> z, которому удовлетворяют документы, содержащие две отдельные последовательности x z и y z. (Этот конкретный запрос в таком виде, как он записан, не имеет смысла, так как x и y не могут находиться в одном месте; но в более сложных ситуациях, например, с шаблонами поиска по маске, запросы этого вида могут быть полезны.)

### 12.1.3. Конфигурации
До этого мы рассматривали очень простые примеры поиска текста. Как было упомянуто выше, весь функционал текстового поиска позволяет делать гораздо больше: пропускать определённые слова (стоп-слова), обрабатывать синонимы и выполнять сложный анализ слов, например, выделять фрагменты не только по пробелам. Все эти функции управляются конфигурациями текстового поиска. В Postgres Pro есть набор предопределённых конфигураций для многих языков, но вы также можете создавать собственные конфигурации. (Все доступные конфигурации можно просмотреть с помощью команды \dF в psql.)

Подходящая конфигурация для данной среды выбирается во время установки и записывается в параметре default_text_search_config в postgresql.conf. Если вы используете для всего кластера одну конфигурацию текстового поиска, вам будет достаточно этого параметра в postgresql.conf. Если же требуется использовать в кластере разные конфигурации, но для каждой базы данных одну определённую, её можно задать командой ALTER DATABASE ... SET. В противном случае конфигурацию можно выбрать в рамках сеанса, определив параметр default_text_search_config.

У каждой функции текстового поиска, зависящей от конфигурации, есть необязательный аргумент regconfig, в котором можно явно указать конфигурацию для данной функции. Значение default_text_search_config используется, только когда этот аргумент опущен.

Для упрощения создания конфигураций текстового поиска они строятся из более простых объектов. В Postgres Pro есть четыре типа таких объектов:

- Анализаторы текстового поиска разделяют документ на фрагменты и классифицируют их (например, как слова или числа).

- Словари текстового поиска приводят фрагменты к нормализованной форме и отбрасывают стоп-слова.

- Шаблоны текстового поиска предоставляют функции, образующие реализацию словарей. (При создании словаря просто задаётся шаблон и набор параметров для него.)

- Конфигурации текстового поиска выбирают анализатор и набор словарей, который будет использоваться для нормализации фрагментов, выданных анализатором.

Анализаторы и шаблоны текстового поиска строятся из низкоуровневых функций на языке C; чтобы создать их, нужно программировать на C, а подключить их к базе данных может только суперпользователь. (В подкаталоге contrib/ инсталляции Postgres Pro можно найти примеры дополнительных анализаторов и шаблонов.) Так как словари и конфигурации представляют собой просто наборы параметров, связывающие анализаторы и шаблоны, их можно создавать, не имея административных прав. Далее в этой главе будут приведены примеры их создания.

## 12.2. Таблицы и индексы
12.2.1. Поиск в таблице
12.2.2. Создание индексов
В предыдущем разделе приводились примеры, которые показывали, как можно выполнить сопоставление с простыми текстовыми константами. В этом разделе показывается, как находить текст в таблице, возможно с применением индексов.

### 12.2.1. Поиск в таблице
Полнотекстовый поиск можно выполнить, не применяя индекс. Следующий простой запрос выводит заголовок (title) каждой строки, содержащей слово friend в поле body:

    SELECT title
    FROM pgweb
    WHERE to_tsvector('english', body) @@ to_tsquery('english', 'friend');
При этом также будут найдены связанные слова, такие как friends и friendly, так как все они сводятся к одной нормализованной лексеме.

В показанном выше примере для разбора и нормализации строки явно выбирается конфигурация english. Хотя параметры, задающие конфигурацию, можно опустить:

    SELECT title
    FROM pgweb
    WHERE to_tsvector(body) @@ to_tsquery('friend');
Такой запрос будет использовать конфигурацию, заданную в параметре default_text_search_config.

В следующем более сложном примере выбираются десять документов, изменённых последними, со словами create и table в полях title или body:

    SELECT title
    FROM pgweb
    WHERE to_tsvector(title || ' ' || body) @@ to_tsquery('create & table')
    ORDER BY last_mod_date DESC
    LIMIT 10;
Чтобы найти строки, содержащие NULL в одном из полей, нужно воспользоваться функцией coalesce, но здесь мы опустили её вызовы для краткости.

Хотя такие запросы будут работать и без индекса, для большинства приложений скорость будет неприемлемой; этот подход рекомендуется только для нерегулярного поиска и динамического содержимого. Для практического применения полнотекстового поиска обычно создаются индексы.

### 12.2.2. Создание индексов
Для ускорения текстового поиска мы можем создать индекс GIN (см. Раздел 12.9):

    CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', body));
Заметьте, что здесь используется функция to_tsvector с двумя аргументами. В выражениях, определяющих индексы, можно использовать только функции, в которых явно задаётся имя конфигурации текстового поиска (см. Раздел 11.7). Это объясняется тем, что содержимое индекса не должно зависеть от значения параметра default_text_search_config. В противном случае содержимое индекса может быть неактуальным, если разные его элементы tsvector будут создаваться с разными конфигурациями текстового поиска и нельзя будет понять, какую именно использовать. Выгрузить и восстановить такой индекс будет невозможно.

Так как при создании индекса использовалась версия to_tsvector с двумя аргументами, этот индекс будет использоваться только в запросах, где to_tsvector вызывается с двумя аргументами и во втором передаётся имя той же конфигурации. То есть, WHERE to_tsvector('english', body) @@ 'a & b' сможет использовать этот индекс, а WHERE to_tsvector(body) @@ 'a & b' — нет. Это гарантирует, что индекс будет использоваться только с той конфигурацией, с которой создавались его элементы.

Индекс можно создать более сложным образом, определив для него имя конфигурации в другом столбце таблицы, например:

    CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector(config_name, body));
где config_name — имя столбца в таблице pgweb. Так можно сохранить имя конфигурации, связанной с элементом индекса, и, таким образом, иметь в одном индексе элементы с разными конфигурациями. Это может быть полезно, например, когда в коллекции документов хранятся документы на разных языках. И в этом случае в запросах должен использоваться тот же индекс (с таким же образом задаваемой конфигурацией), например, так: WHERE to_tsvector(config_name, body) @@ 'a & b'.

Индексы могут создаваться даже по объединению столбцов:

    CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));
Ещё один вариант — создать отдельный столбец tsvector, в котором сохранить результат to_tsvector. Чтобы этот столбец автоматически синхронизировался с исходными данными, он создаётся как сохранённый генерируемый столбец. Следующий пример показывает, как можно подготовить для индексации объединённое содержимое столбцов title и body, применив функцию coalesce для получения желаемого результата, даже когда один из столбцов NULL:

    ALTER TABLE pgweb
        ADD COLUMN textsearchable_index_col tsvector
                GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) STORED;
    Затем мы создаём индекс GIN для ускорения поиска:

    CREATE INDEX textsearch_idx ON pgweb USING GIN (textsearchable_index_col);
Теперь мы можем быстро выполнять полнотекстовый поиск:

    SELECT title
    FROM pgweb
    WHERE textsearchable_index_col @@ to_tsquery('create & table')
    ORDER BY last_mod_date DESC
    LIMIT 10;
Хранение вычисленного выражения индекса в отдельном столбце даёт ряд преимуществ. Во-первых, для использования индекса в запросах не нужно явно указывать имя конфигурации текстового поиска. Как показано в вышеприведённом примере, в этом случае запрос может зависеть от default_text_search_config. Во-вторых, поиск выполняется быстрее, так как для проверки соответствия данных индексу не нужно повторно выполнять to_tsvector. (Это актуально больше для индексов GiST, чем для GIN; см. Раздел 12.9.) С другой стороны, схему с индексом по выражению проще реализовать и она позволяет сэкономить место на диске, так как представление tsvector не хранится явно.

## 12.3. Управление текстовым поиском
12.3.1. Разбор документов
12.3.2. Разбор запросов
12.3.3. Ранжирование результатов поиска
12.3.4. Выделение результатов
Для реализации полнотекстового поиска необходимы функции, позволяющие создать tsvector из документа и tsquery из запроса пользователя. Кроме того, результаты нужно выдавать в удобном порядке, так что нам потребуется функция, оценивающая релевантность документа для данного запроса. Важно также иметь возможность выводить найденный текст подходящим образом. В Postgres Pro есть все необходимые для этого функции.

### 12.3.1. Разбор документов
Для преобразования документа в тип tsvector Postgres Pro предоставляет функцию to_tsvector.

    to_tsvector([конфигурация regconfig,] документ text) returns tsvector
to_tsvector разбирает текстовый документ на фрагменты, сводит фрагменты к лексемам и возвращает значение tsvector, в котором перечисляются лексемы и их позиции в документе. При обработке документа используется указанная конфигурация текстового поиска или конфигурация по умолчанию. Простой пример:

    SELECT to_tsvector('english', 'a fat  cat sat on a mat - it ate a fat rats');
                    to_tsvector
    -----------------------------------------------------
    'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
В этом примере мы видим, что результирующий tsvector не содержит слова a, on и it, слово rats превратилось rat, а знак препинания «-» был проигнорирован.

Функция to_tsvector внутри вызывает анализатор, который разбивает текст документа на фрагменты и классифицирует их. Для каждого фрагмента она проверяет список словарей (Раздел 12.6), определяемый типом фрагмента. Первый же словарь, распознавший фрагмент, выдаёт одну или несколько представляющих его лексем. Например, rats превращается в rat, так как один из словарей понимает, что слово rats — это слово rat во множественном числе. Некоторое слова распознаются как стоп-слова (Подраздел 12.6.1) и игнорируются как слова, фигурирующие в тексте настолько часто, что искать их бессмысленно. В нашем примере это a, on и it. Если фрагмент не воспринимается ни одним словарём из списка, он так же игнорируется. В данном примере это происходит со знаком препинания -, так как с таким типом фрагмента (символы-разделители) не связан никакой словарь и значит такие фрагменты никогда не будут индексироваться. Выбор анализатора, словарей и индексируемых типов фрагментов определяется конфигурацией текстового поиска (Раздел 12.7). В одной базе данных можно использовать разные конфигурации, в том числе, предопределённые конфигурации для разных языков. В нашем примере мы использовали конфигурацию по умолчанию для английского языка — english.

Для назначения элементам tsvector разных весов используется функция setweight. Вес элемента задаётся буквой A, B, C или D. Обычно это применяется для обозначения важности слов в разных частях документа, например в заголовке или в теле документа. Затем эта информация может использоваться при ранжировании результатов поиска.

Так как to_tsvector(NULL) вернёт NULL, мы советуем использовать coalesce везде, где соответствующее поле может быть NULL. Создавать tsvector из структурированного документа рекомендуется так:

    UPDATE tt SET ti =
        setweight(to_tsvector(coalesce(title,'')), 'A')    ||
        setweight(to_tsvector(coalesce(keyword,'')), 'B')  ||
        setweight(to_tsvector(coalesce(abstract,'')), 'C') ||
        setweight(to_tsvector(coalesce(body,'')), 'D');
Здесь мы использовали setweight для пометки происхождения каждой лексемы в сформированных значениях tsvector и объединили помеченные значения с помощью оператора конкатенации типов tsvector ||. (Подробнее эти операции рассматриваются в Подразделе 12.4.1.)

### 12.3.2. Разбор запросов
Postgres Pro предоставляет функции to_tsquery, plainto_tsquery, phraseto_tsquery и websearch_to_tsquery для приведения запроса к типу tsquery. Функция to_tsquery даёт больше возможностей, чем plainto_tsquery или phraseto_tsquery, но более строга к входным данным. Функция websearch_to_tsquery представляет собой упрощённую версию to_tsquery с альтернативным синтаксисом, подобным тому, что принят в поисковых системах в Интернете.

    to_tsquery([конфигурация regconfig,] текст_запроса text) returns tsquery
to_tsquery создаёт значение tsquery из текста_запроса, который может состоять из простых фрагментов, разделённых логическими операторами tsquery: & (И), | (ИЛИ), ! (НЕ) и <-> (ПРЕДШЕСТВУЕТ), возможно, сгруппированных скобками. Другими словами, входное значение для to_tsquery должно уже соответствовать общим правилам для значений tsquery, описанным в Подразделе 8.11.2. Различие их состоит в том, что во вводимом в tsquery значении фрагменты воспринимаются буквально, тогда как to_tsquery нормализует фрагменты, приводя их к лексемам, используя явно указанную или подразумеваемую конфигурацию, и отбрасывая стоп-слова. Например:

    SELECT to_tsquery('english', 'The & Fat & Rats');
    to_tsquery   
    ---------------
    'fat' & 'rat'
Как и при вводе значения tsquery, для каждой лексемы можно задать вес(а), чтобы при поиске можно было выбрать из tsvector только лексемы с заданными весами. Например:

    SELECT to_tsquery('english', 'Fat | Rats:AB');
        to_tsquery    
    ------------------
    'fat' | 'rat':AB
    К лексеме также можно добавить *, определив таким образом условие поиска по префиксу:

    SELECT to_tsquery('supern:*A & star:A*B');
            to_tsquery        
    --------------------------
    'supern':*A & 'star':*AB
Такая лексема будет соответствовать любому слову в tsvector, начинающемуся с данной подстроки.

to_tsquery может также принимать фразы в апострофах. Это полезно в основном когда конфигурация включает тезаурус, который может обрабатывать такие фразы. В показанном ниже примере предполагается, что тезаурус содержит правило supernovae stars : sn:

    SELECT to_tsquery('''supernovae stars'' & !crab');
    to_tsquery
    ---------------
    'sn' & !'crab'
Если убрать эти апострофы, to_tsquery не примет фрагменты, не разделённые операторами И, ИЛИ и ПРЕДШЕСТВУЕТ, и выдаст синтаксическую ошибку.

    plainto_tsquery([конфигурация regconfig,] текст_запроса text) returns tsquery
plainto_tsquery преобразует неформатированный текст_запроса в значение tsquery. Текст разбирается и нормализуется подобно тому, как это делает to_tsvector, а затем между оставшимися словами вставляются операторы & (И) типа tsquery.

Пример:

    SELECT plainto_tsquery('english', 'The Fat Rats');
    plainto_tsquery 
    -----------------
    'fat' & 'rat'
Заметьте, что plainto_tsquery не распознает во входной строке операторы tsquery, метки весов или обозначения префиксов:

    SELECT plainto_tsquery('english', 'The Fat & Rats:C');
    plainto_tsquery   
    ---------------------
    'fat' & 'rat' & 'c'
В данном случае все знаки пунктуации были отброшены как символы-разделители.

    phraseto_tsquery([конфигурация regconfig,] текст_запроса text) returns tsquery

phraseto_tsquery ведёт себя подобно plainto_tsquery, за исключением того, что она вставляет между оставшимися словами оператор <-> (ПРЕДШЕСТВУЕТ) вместо оператора & (И). Кроме того, стоп-слова не просто отбрасываются, а подсчитываются, и вместо операторов <-> используются операторы <N> с подсчитанным числом. Эта функция полезна при поиске точных последовательностей лексем, так как операторы ПРЕДШЕСТВУЕТ проверяют не только наличие всех лексем, но и их порядок.

Пример:

    SELECT phraseto_tsquery('english', 'The Fat Rats');
    phraseto_tsquery
    ------------------
    'fat' <-> 'rat'
    Как и plainto_tsquery, функция phraseto_tsquery не распознает во входной строке операторы типа tsquery, метки весов или обозначения префиксов:

    SELECT phraseto_tsquery('english', 'The Fat & Rats:C');
        phraseto_tsquery
    -----------------------------
    'fat' <-> 'rat' <-> 'c'
websearch_to_tsquery([конфигурация regconfig,] текст_запроса text) returns tsquery
Функция websearch_to_tsquery создаёт значение tsquery из текста_запроса, используя альтернативный синтаксис, в котором запрос задаётся просто неформатированным текстом. В отличие от plainto_tsquery и phraseto_tsquery, она также принимает определённые операторы. Более того, эта функция не должна никогда выдавать синтаксические ошибки, что позволяет осуществлять поиск по произвольному заданному пользователем запросу. Она поддерживает следующий синтаксис:

текст не в кавычках: текст, не заключённый в кавычки, который будет преобразован в слова, разделяемые операторами &, как его восприняла бы функция plainto_tsquery.

"текст в кавычках": текст, заключённый в кавычки, будет преобразован в слова, разделяемые операторами <->, как его восприняла бы функция phraseto_tsquery.

    OR: логическая операция ИЛИ будет преобразована в оператор |.

    -: логическая операция НЕ, преобразуется в оператор !.

Примеры:

    SELECT websearch_to_tsquery('english', 'The fat rats');
    websearch_to_tsquery
    ----------------------
    'fat' & 'rat'
    (1 row)

    SELECT websearch_to_tsquery('english', '"supernovae stars" -crab');
        websearch_to_tsquery
    ----------------------------------
    'supernova' <-> 'star' & !'crab'
    (1 row)

    SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"');
        websearch_to_tsquery
    -----------------------------------
    'sad' <-> 'cat' | 'fat' <-> 'rat'
    (1 row)

    SELECT websearch_to_tsquery('english', 'signal -"segmentation fault"');
            websearch_to_tsquery
    ---------------------------------------
    'signal' & !( 'segment' <-> 'fault' )
    (1 row)

    SELECT websearch_to_tsquery('english', '""" )( dummy \\ query <->');
    websearch_to_tsquery
    ----------------------
    'dummi' & 'queri'
    (1 row)
### 12.3.3. Ранжирование результатов поиска
Ранжирование документов можно представить как попытку оценить, насколько они релевантны заданному запросу и отсортировать их так, чтобы наиболее релевантные выводились первыми. В Postgres Pro встроены две функции ранжирования, принимающие во внимание лексическую, позиционную и структурную информацию; то есть, они учитывают, насколько часто и насколько близко встречаются в документе ключевые слова и какова важность содержащей их части документа. Однако само понятие релевантности довольно размытое и во многом определяется приложением. Приложения могут использовать для ранжирования и другую информацию, например, время изменения документа. Встроенные функции ранжирования можно рассматривать лишь как примеры реализации. Для своих конкретных задач вы можете разработать собственные функции ранжирования и/или учесть при обработке их результатов дополнительные факторы.

Ниже описаны две встроенные функции ранжирования:

    ts_rank([веса float4[],] вектор tsvector, запрос tsquery [, нормализация integer]) returns float4
Ранжирует векторы по частоте найденных лексем.

    ts_rank_cd([веса float4[],] вектор tsvector, запрос tsquery [, нормализация integer]) returns float4
Эта функция вычисляет плотность покрытия для данного вектора документа и запроса, используя метод, разработанный Кларком, Кормаком и Тадхоуп и описанный в статье "Relevance Ranking for One to Three Term Queries" в журнале "Information Processing and Management" в 1999 г. Плотность покрытия вычисляется подобно рангу ts_rank, но в расчёт берётся ещё и близость соответствующих лексем друг к другу.

Для вычисления результата этой функции требуется информация о позиции лексем. Поэтому она игнорируют «очищенные» от этой информации лексемы в tsvector. Если во входных данных нет неочищенных лексем, результат будет равен нулю. (За дополнительными сведениями о функции strip и позиционной информации в данных tsvector обратитесь к Подразделу 12.4.1.)

Для обеих этих функций аргумент веса позволяет придать больший или меньший вес словам, в зависимости от их меток. В передаваемом массиве весов определяется, насколько весома каждая категория слов, в следующем порядке:

    {вес D, вес C, вес B, вес A}
Если этот аргумент опускается, подразумеваются следующие значения:

    {0.1, 0.2, 0.4, 1.0}
Обычно весами выделяются слова из особых областей документа, например из заголовка или краткого введения, с тем, чтобы эти слова считались более и менее значимыми, чем слова в основном тексте документа.

Так как вероятность найти ключевые слова увеличивается с размером документа, при ранжировании имеет смысл учитывать его, чтобы, например, документ с сотней слов, содержащий пять вхождений искомых слов, считался более релевантным, чем документ с тысячей слов и теми же пятью вхождениями. Обе функции ранжирования принимают целочисленный параметр нормализации, определяющий, как ранг документа будет зависеть от его размера. Этот параметр представляет собой битовую маску и управляет несколькими режимами: вы можете включить сразу несколько режимов, объединив значения оператором | (например так: 2|4).

- 0 (по умолчанию): длина документа не учитывается

- 1: ранг документа делится на 1 + логарифм длины документа

- 2: ранг документа делится на его длину

- 4: ранг документа делится на среднее гармоническое расстояние между блоками (это реализовано только в ts_rank_cd)

- 8: ранг документа делится на число уникальных слов в документе

- 16: ранг документа делится на 1 + логарифм числа уникальных слов в документе

- 32: ранг делится своё же значение + 1

Если включены несколько флагов, соответствующие операции выполняются в показанном порядке.

Важно заметить, что функции ранжирования не используют никакую внешнюю информацию, так что добиться нормализации до 1% или 100% невозможно, хотя иногда это желательно. Применив параметр 32 (rank/(rank+1)), можно свести все ранги к диапазону 0..1, но это изменение будет лишь косметическим, на порядке сортировки результатов это не отразится.

В данном примере выбираются десять найденных документов с максимальным рангом:

    SELECT title, ts_rank_cd(textsearch, query) AS rank
    FROM apod, to_tsquery('neutrino|(dark & matter)') query
    WHERE query @@ textsearch
    ORDER BY rank DESC
    LIMIT 10;
                        title                     |   rank
    -----------------------------------------------+----------
    Neutrinos in the Sun                          |      3.1
    The Sudbury Neutrino Detector                 |      2.4
    A MACHO View of Galactic Dark Matter          |  2.01317
    Hot Gas and Dark Matter                       |  1.91171
    The Virgo Cluster: Hot Plasma and Dark Matter |  1.90953
    Rafting for Solar Neutrinos                   |      1.9
    NGC 4650A: Strange Galaxy and Dark Matter     |  1.85774
    Hot Gas and Dark Matter                       |   1.6123
    Ice Fishing for Cosmic Neutrinos              |      1.6
    Weak Lensing Distorts the Universe            | 0.818218
Тот же пример с нормализованным рангом:

    SELECT title, ts_rank_cd(textsearch, query, 32 /* rank/(rank+1) */ ) AS rank
    FROM apod, to_tsquery('neutrino|(dark & matter)') query
    WHERE  query @@ textsearch
    ORDER BY rank DESC
    LIMIT 10;
                        title                     |        rank
    -----------------------------------------------+-------------------
    Neutrinos in the Sun                          | 0.756097569485493
    The Sudbury Neutrino Detector                 | 0.705882361190954
    A MACHO View of Galactic Dark Matter          | 0.668123210574724
    Hot Gas and Dark Matter                       |  0.65655958650282
    The Virgo Cluster: Hot Plasma and Dark Matter | 0.656301290640973
    Rafting for Solar Neutrinos                   | 0.655172410958162
    NGC 4650A: Strange Galaxy and Dark Matter     | 0.650072921219637
    Hot Gas and Dark Matter                       | 0.617195790024749
    Ice Fishing for Cosmic Neutrinos              | 0.615384618911517
    Weak Lensing Distorts the Universe            | 0.450010798361481
Ранжирование может быть довольно дорогостоящей операцией, так как для вычисления ранга необходимо прочитать tsvector каждого подходящего документа и это займёт значительное время, если придётся обращаться к диску. К сожалению, избежать этого вряд ли возможно, так как на практике по многим запросам выдаётся большое количество результатов.

### 12.3.4. Выделение результатов
Представляя результаты поиска, в идеале нужно выделять часть документа и показывать, как он связан с запросом. Обычно поисковые системы показывают фрагменты документа с отмеченными искомыми словами. В Postgres Pro для реализации этой возможности представлена функция ts_headline.

    ts_headline([конфигурация regconfig,] документ text, запрос tsquery [, параметры text])
    returns text
ts_headline принимает документ вместе с запросом и возвращает выдержку из документа, в которой выделяются слова из запроса. Применяемую для разбора документа конфигурацию можно указать в параметре config; если этот параметр опущен, применяется конфигурация default_text_search_config.

Если в параметрах передаётся строка options, она должна состоять из списка разделённых запятыми пар параметр=значение. Параметры могут быть следующими:

- StartSel, StopSel: строки, которые будут разграничивать слова запроса в документе, выделяя их среди остальных. Если эти строки содержат пробелы или запятые, их нужно заключить в кавычки.

- MaxWords, MinWords: эти числа определяет нижний и верхний предел размера выдержки.

- ShortWord: слова такой длины или короче в начале и конце выдержки будут отбрасываться. Значение по умолчанию, равное 3, исключает распространённые английские артикли.

- HighlightAll: логический флаг; если он равен true, выдержкой будет весь документ и три предыдущие параметра игнорируются.

- MaxFragments: максимальное число выводимых текстовых выдержек или фрагментов. Значение по умолчанию, равное 0, выбирает метод создания выдержки без фрагментов. При значении большем 0 выбирается метод с фрагментами, когда находятся все фрагменты, содержащие как можно больше слов запроса, а затем они сжимаются до слов запроса. Такие фрагменты могут содержать какие-то ключевые слова в середине и ограничиваются двумя искомыми словами. При этом фрагменты могут содержать не больше MaxWords слов, а в начале и конце они будут очищены от слов длины ShortWord и меньше. Если в документе найдены не все слова запроса, выводится один фрагмент, включающий первые MinWords слов в документе.

- FragmentDelimiter: Когда выводятся несколько фрагментов, они будут разделяться этой строкой.

Имена этих параметров распознаются без учёта регистра. Явно не определённые параметры получают такие значения по умолчанию:

    StartSel=<b>, StopSel=</b>,
    MaxWords=35, MinWords=15, ShortWord=3, HighlightAll=FALSE,
    MaxFragments=0, FragmentDelimiter=" ... "
Пример использования:

    SELECT ts_headline('english',
    'The most common type of search
    is to find all documents containing given query terms
    and return them in order of their similarity to the
    query.',
    to_tsquery('query & similarity'));
                            ts_headline                         
    ------------------------------------------------------------
    containing given <b>query</b> terms
    and return them in order of their <b>similarity</b> to the
    <b>query</b>.

    SELECT ts_headline('english',
    'The most common type of search
    is to find all documents containing given query terms
    and return them in order of their similarity to the
    query.',
    to_tsquery('query & similarity'),
    'StartSel = <, StopSel = >');
                        ts_headline                      
    -------------------------------------------------------
    containing given <query> terms
    and return them in order of their <similarity> to the
    <query>.
Функция ts_headline работает с оригинальным документом, а не его сжатым представлением tsvector, так что она может быть медленной и использовать её следует осмотрительно.

12.4. Дополнительные возможности
12.4.1. Обработка документов
12.4.2. Обработка запросов
12.4.3. Триггеры для автоматического обновления
12.4.4. Сбор статистики по документу
В этом разделе описываются дополнительные функции и операторы, которые могут быть полезны при поиске текста.

12.4.1. Обработка документов
В Подразделе 12.3.1 показывалось, как обычные текстовые документы можно преобразовать в значения tsvector. Postgres Pro предлагает также набор функций и операторов для обработки документов, уже представленных в формате tsvector.

tsvector || tsvector
Оператор конкатенации значений tsvector возвращает вектор, объединяющий лексемы и позиционную информацию двух векторов, переданных ему в аргументах. В полученном результате сохраняются позиции и метки весов. При этом позиции в векторе справа сдвигаются на максимальное значение позиции в векторе слева, что почти равносильно применению to_tsvector к результату конкатенации двух исходных строк документов. (Почти, потому что стоп-слова, исключаемые в конце левого аргумента, при конкатенации исходных строк влияют на позиции лексем в правой части, а при конкатенации tsvector — нет.)

Преимущество же конкатенации документов в векторной форме по сравнению с конкатенацией текста до вызова to_tsvector заключается в том, что так можно разбирать разные части документа, применяя разные конфигурации. И так как функция setweight помечает все лексемы данного вектора одинаково, разбирать текст и выполнять setweight нужно до объединения разных частей документа с подразумеваемым разным весом.

setweight(вектор tsvector, вес "char") returns tsvector
setweight возвращает копию входного вектора, помечая в ней каждую позицию заданным весом, меткой A, B, C или D. (Метка D по умолчанию назначается всем векторам, так что при выводе она опускается.) Эти метки сохраняются при конкатенации векторов, что позволяет придавать разные веса словам из разных частей документа и, как следствие, ранжировать их по-разному.

Заметьте, что веса назначаются позициям, а не лексемам. Если входной вектор очищен от позиционной информации, setweight не делает ничего.

length(вектор tsvector) returns integer
Возвращает число лексем, сохранённых в векторе.

strip(вектор tsvector) returns tsvector
Возвращает вектор с теми же лексемами, что и в данном, но без информации о позиции и весе. Очищенный вектор обычно оказывается намного меньше исходного, но при этом и менее полезным. С очищенными векторами хуже работает ранжирование, а также оператор <-> (ПРЕДШЕСТВУЕТ) типа tsquery никогда не найдёт соответствие в них, так как не сможет определить расстояние между вхождениями лексем.

Полный список связанных с tsvector функций приведён в Таблице 9.42.

### 12.4.2. Обработка запросов
В Подразделе 12.3.2 показывалось, как обычные текстовые запросы можно преобразовывать в значения tsquery. Postgres Pro предлагает также набор функций и операторов для обработки запросов, уже представленных в формате tsquery.

#### tsquery && tsquery
Возвращает логическое произведение (AND) двух данных запросов.

#### tsquery || tsquery
Возвращает логическое объединение (OR) двух данных запросов.

#### !! tsquery
Возвращает логическое отрицание (NOT) данного запроса.

#### tsquery <-> tsquery
Возвращает запрос, который ищет соответствие первому данному запросу, за которым следует соответствие второму данному запросу, с применением оператора <-> (ПРЕДШЕСТВУЕТ) типа tsquery. Например:

    SELECT to_tsquery('fat') <-> to_tsquery('cat | rat');
                ?column?
    -----------------------------------
    'fat' <-> 'cat' | 'fat' <-> 'rat'
#### tsquery_phrase(запрос1 tsquery, запрос2 tsquery [, расстояние integer ]) returns tsquery
Возвращает запрос, который ищет соответствие первому данному запросу, за которым следует соответствие второму данному запросу (число лексем между ними задаётся параметром расстояние), с применением оператора <N> типа tsquery. Например:

    SELECT tsquery_phrase(to_tsquery('fat'), to_tsquery('cat'), 10);
    tsquery_phrase
    ------------------
    'fat' <10> 'cat'
#### numnode(запрос tsquery) returns integer
Возвращает число узлов (лексем и операторов) в значении tsquery. Эта функция помогает определить, имеет ли смысл запрос (тогда её результат > 0) или он содержит только стоп-слова (тогда она возвращает 0). Примеры:

    SELECT numnode(plainto_tsquery('the any'));
    ЗАМЕЧАНИЕ:  запрос поиска текста игнорируется, так как содержит
    только стоп-слова или не содержит лексем
    numnode
    ---------
        0

    SELECT numnode('foo & bar'::tsquery);
    numnode
    ---------
        3
#### querytree(запрос tsquery) returns text
Возвращает часть tsquery, которую можно использовать для поиска по индексу. Эта функция помогает выявить неиндексируемые запросы, например, такие, которые содержат только стоп-слова или условия отрицания. Например:

    SELECT querytree(to_tsquery('!defined'));
    querytree
    -----------

#### 12.4.2.1. Перезапись запросов
Семейство запросов ts_rewrite ищет в данном tsquery вхождения целевого подзапроса и заменяет каждое вхождение указанной подстановкой. По сути эта операция похожа на замену подстроки в строке, только рассчитана на работу с tsquery. Сочетание целевого подзапроса с подстановкой можно считать правилом перезаписи запроса. Набор таких правил перезаписи может быть очень полезен при поиске. Например, вы можете улучшить результаты, добавив синонимы (например, big apple, nyc и gotham для new york) или сузить область поиска, чтобы нацелить пользователя на некоторую область. Это в некотором смысле пересекается с функциональностью тезаурусов (Подраздел 12.6.4). Однако, при таком подходе вы можете изменять правила перезаписи «на лету», тогда как при обновлении тезауруса необходима переиндексация.

    ts_rewrite (запрос tsquery, цель tsquery, замена tsquery) returns tsquery
Эта форма ts_rewrite просто применяет одно правило перезаписи: цель заменяется подстановкой везде, где она находится в запросе. Например:

    SELECT ts_rewrite('a & b'::tsquery, 'a'::tsquery, 'c'::tsquery);
    ts_rewrite
    ------------
    'b' & 'c'
    ts_rewrite (запрос tsquery, выборка text) returns tsquery
Эта форма ts_rewrite принимает начальный запрос и SQL-команду select, которая задаётся текстовой строкой. Команда select должна выдавать два столбца типа tsquery. Для каждой строки результата select вхождения первого столбца (цели) заменяются значениями второго столбца (подстановкой) в тексте запроса. Например:

    CREATE TABLE aliases (t tsquery PRIMARY KEY, s tsquery);
    INSERT INTO aliases VALUES('a', 'c');

    SELECT ts_rewrite('a & b'::tsquery, 'SELECT t,s FROM aliases');
    ts_rewrite
    ------------
    'b' & 'c'
Заметьте, что когда таким способом применяются несколько правил перезаписи, порядок их применения может иметь значение, поэтому в исходном запросе следует добавить ORDER BY по какому-либо ключу.

Давайте рассмотрим практический пример на тему астрономии. Мы развернём запрос supernovae, используя правила перезаписи в таблице:

    CREATE TABLE aliases (t tsquery primary key, s tsquery);
    INSERT INTO aliases VALUES(to_tsquery('supernovae'),
    to_tsquery('supernovae|sn'));

    SELECT ts_rewrite(to_tsquery('supernovae & crab'), 'SELECT * FROM aliases');
            ts_rewrite            
    ---------------------------------
    'crab' & ( 'supernova' | 'sn' )
Мы можем скорректировать правила перезаписи, просто изменив таблицу:

    UPDATE aliases
    SET s = to_tsquery('supernovae|sn & !nebulae')
    WHERE t = to_tsquery('supernovae');

    SELECT ts_rewrite(to_tsquery('supernovae & crab'), 'SELECT * FROM aliases');
                    ts_rewrite                  
    ---------------------------------------------
    'crab' & ( 'supernova' | 'sn' & !'nebula' )
Перезапись может быть медленной, когда задано много правил перезаписи, так как соответствия будут проверяться для каждого правила. Чтобы отфильтровать явно неподходящие правила, можно использовать проверки включения для типа tsquery. В следующем примере выбираются только те правила, которые могут соответствовать исходному запросу:

    SELECT ts_rewrite('a & b'::tsquery,
                    'SELECT t,s FROM aliases WHERE ''a & b''::tsquery @> t');
    ts_rewrite
    ------------
    'b' & 'c'
### 12.4.3. Триггеры для автоматического обновления
> Примечание
Описанный в этом разделе подход считается устаревшим в связи с появлением возможности использовать генерируемые столбцы, как описано в Подразделе 12.2.2.

Когда представление документа в формате tsvector хранится в отдельном столбце, необходимо создать триггер, который будет обновлять его содержимое при изменении столбцов, из которых составляется исходный документ. Для этого можно использовать две встроенные триггерные функции или написать свои собственные.

    tsvector_update_trigger(столбец_tsvector, имя_конфигурации, столбец_текста [, ...])
    tsvector_update_trigger_column(столбец_tsvector, столбец_конфигурации,
    столбец_текста [, ...])
Эти триггерные функции автоматически вычисляют значение для столбца tsvector из одного или нескольких текстовых столбцов с параметрами, указанными в команде CREATE TRIGGER. Пример их использования:

    CREATE TABLE messages (
        title       text,
        body        text,
        tsv         tsvector
    );

    CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
    ON messages FOR EACH ROW EXECUTE FUNCTION
    tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);

    INSERT INTO messages VALUES('title here', 'the body text is here');

    SELECT * FROM messages;
    title    |         body          |            tsv             
    ------------+-----------------------+----------------------------
    title here | the body text is here | 'bodi':4 'text':5 'titl':1

    SELECT title, body FROM messages WHERE tsv @@ to_tsquery('title & body');
    title    |         body          
    ------------+-----------------------
    title here | the body text is here
С таким триггером любое изменение в полях title или body будет автоматически отражаться в содержимом tsv, так что приложению не придётся заниматься этим.

Первым аргументом этих функций должно быть имя столбца tsvector, содержимое которого будет обновляться. Ещё один аргумент — конфигурация текстового поиска, которая будет использоваться для преобразования. Для tsvector_update_trigger имя конфигурации передаётся просто как второй аргумент триггера. Это имя должно быть определено полностью, чтобы поведение триггера не менялось при изменениях в пути поиска (search_path). Для tsvector_update_trigger_column во втором аргументе триггера передаётся имя другого столбца таблицы, который должен иметь тип regconfig. Это позволяет использовать разные конфигурации для разных строк. В оставшихся аргументах передаются имена текстовых столбцов (типа text, varchar или char). Их содержимое будет включено в документ в заданном порядке. При этом значения NULL будут пропущены (а другие столбцы будут индексироваться).

Ограничение этих встроенных триггеров заключается в том, что они обрабатывают все столбцы одинаково. Чтобы столбцы обрабатывались по-разному, например для текста заголовка задавался не тот же вес, что для тела документа, потребуется разработать свой триггер. К примеру, так это можно сделать на языке PL/pgSQL:

    CREATE FUNCTION messages_trigger() RETURNS trigger AS $$
    begin
    new.tsv :=
        setweight(to_tsvector('pg_catalog.english', coalesce(new.title,'')), 'A') ||
        setweight(to_tsvector('pg_catalog.english', coalesce(new.body,'')), 'D');
    return new;
    end
    $$ LANGUAGE plpgsql;

    CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
        ON messages FOR EACH ROW EXECUTE FUNCTION messages_trigger();
Помните, что, создавая значения tsvector в триггерах, важно явно указывать имя конфигурации, чтобы содержимое столбца не зависело от изменений default_text_search_config. В противном случае могут возникнуть проблемы, например результаты поиска изменятся после выгрузки и восстановления данных.

### 12.4.4. Сбор статистики по документу
Функция ts_stat может быть полезна для проверки конфигурации и нахождения возможных стоп-слов.

    ts_stat(sql_запрос text, [веса text,]
            OUT слово text, OUT число_док integer,
            OUT число_вхожд integer) returns setof record
Здесь sql_запрос — текстовая строка, содержащая SQL-запрос, который должен возвращать один столбец tsvector. Функция ts_stat выполняет запрос и возвращает статистику по каждой отдельной лексеме (слову), содержащейся в данных tsvector. Её результат представляется в столбцах

- слово text — значение лексемы

- число_док integer — число документов (значений tsvector), в которых встретилось слово

- число_вхожд integer — общее число вхождений слова

Если передаётся параметр weights, то подсчитываются только вхождения с указанными в нём весами.

Например, найти десять наиболее часто используемых слов в коллекции документов можно так:

    SELECT * FROM ts_stat('SELECT vector FROM apod')
    ORDER BY nentry DESC, ndoc DESC, word
    LIMIT 10;
Следующий запрос возвращает тоже десять слов, но при выборе их учитываются только вхождения с весами A или B:

    SELECT * FROM ts_stat('SELECT vector FROM apod', 'ab')
    ORDER BY nentry DESC, ndoc DESC, word
    LIMIT 10;

## 12.5. Анализаторы
Задача анализаторов текста — разделить текст документа на фрагменты и присвоить каждому из них тип из набора, определённого в самом анализаторе. Заметьте, что анализаторы не меняют текст — они просто выдают позиции предполагаемых слов. Вследствие такой ограниченности их функций, собственные специфические анализаторы бывают нужны гораздо реже, чем собственные словари. В настоящее время в Postgres Pro есть только один встроенный анализатор, который может быть полезен для широкого круга приложений.

Этот встроенный анализатор называется pg_catalog.default. Он распознаёт 23 типа фрагментов, перечисленные в Таблице 12.1.

Таблица 12.1. Типы фрагментов, выделяемых стандартным анализатором

| Псевдоним                                | Описание                                 | Пример                                   |
|------------------------------------------|------------------------------------------|------------------------------------------|
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">asciiword</code> | Слово только из букв ASCII               | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">elephant</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">word</code> | Слово из любых букв                      | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">mañana</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">numword</code> | Слово из букв и цифр                     | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">beta1</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">asciihword</code> | Слово только из букв ASCII с дефисами    | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">up-to-date</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">hword</code> | Слово из любых букв с дефисами           | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">lógico-matemática</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">numhword</code> | Слово из букв и цифр с дефисами          | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">postgresql-beta1</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">hword_asciipart</code> | Часть слова с дефисами, только из букв ASCII | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">postgresql</code><span> </span>в словосочетании<span> </span><code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">postgresql-beta1</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">hword_part</code> | Часть слова с дефисами, из любых букв    | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">lógico</code><span> </span>или<span> </span><code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">matemática</code><span> </span>в словосочетании<span> </span><code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">lógico-matemática</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">hword_numpart</code> | Часть слова с дефисами, из букв и цифр   | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">beta1</code><span> </span>в словосочетании<span> </span><code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">postgresql-beta1</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">email</code> | Адрес электронной почты                  | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">foo@example.com</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">protocol</code> | Префикс протокола                        | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">http://</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">url</code> | URL                                      | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">example.com/stuff/​index.html</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">host</code> | Имя узла                                 | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">example.com</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">url_path</code> | Путь в адресе URL                        | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">/stuff/index.html</code>, как часть URL |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">file</code> | Путь или имя файла                       | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">/usr/local/foo.txt</code>, если не является частью URL |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">sfloat</code> | Научная запись числа                     | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">-1.234e56</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">float</code> | Десятичная запись числа                  | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">-1.234</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">int</code> | Целое со знаком                          | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">-1234</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">uint</code> | Целое без знака                          | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">1234</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">version</code> | Номер версии                             | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">8.3.0</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">tag</code> | Тег XML                                  | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">&lt;a href=​"dictionaries.html"&gt;</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">entity</code> | Сущность XML                             | <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">&amp;amp;</code> |
| <code class="literal" style="box-sizing: border-box; font-family: monospace, monospace; font-size: 1em; padding: 0px; color: rgb(82, 95, 108); background-color: rgb(255, 255, 255); border-radius: 0px;">blank</code> | Символы-разделители                      | (любые пробельные символы или знаки препинания, не попавшие в другие категории) 
|

> Примечание
Понятие «буквы» анализатор определяет исходя из локали, заданной для базы данных, в частности параметра lc_ctype. Слова, содержащие только буквы из ASCII (латинские буквы), распознаются как фрагменты отдельного типа, так как иногда бывает полезно выделить их. Для многих европейских языков типы фрагментов word и asciiword можно воспринимать как синонимы.

email принимает не все символы, которые считаются допустимыми по стандарту RFC 5322. В частности, имя почтового ящика помимо алфавитно-цифровых символов может содержать только точку, минус и подчёркивание.

Анализатор может выделить в одном тексте несколько перекрывающихся фрагментов. Например, слово с дефисом будет выдано как целое составное слово и по частям:

    SELECT alias, description, token FROM ts_debug('foo-bar-beta1');
          alias      |               description                |     token    
    -----------------+------------------------------------------+--------------
     numhword        | Hyphenated word, letters and digits      | foo-bar-beta1
     hword_asciipart | Hyphenated word part, all ASCII          | foo
     blank           | Space symbols                            | -
     hword_asciipart | Hyphenated word part, all ASCII          | bar
     blank           | Space symbols                            | -
     hword_numpart   | Hyphenated word part, letters and digits | beta1
Это поведение считается желательным, так как это позволяет находить при последующем поиске и всё слово целиком, и его части. Ещё один показательный пример:

    SELECT alias, description, token
    FROM ts_debug('http://example.com/stuff/index.html');
      alias   |  description  |            token             
    ----------+---------------+------------------------------
     protocol | Protocol head | http://
     url      | URL           | example.com/stuff/index.html
     host     | Host          | example.com
     url_path | URL path      | /stuff/index.html

## 12.6. Словари
12.6.1. Стоп-слова
12.6.2. Простой словарь
12.6.3. Словарь синонимов
12.6.4. Тезаурус
12.6.5. Словарь Ispell
12.6.6. Словарь Snowball
Словари полнотекстового поиска предназначены для исключения стоп-слов (слов, которые не должны учитываться при поиске) и нормализации слов, чтобы разные словоформы считались совпадающими. Успешно нормализованное слово называется лексемой. Нормализация и исключение стоп-слов не только улучшает качество поиска, но и уменьшает размер представления документа в формате tsvector, и, как следствие, увеличивает быстродействие. Нормализация не всегда имеет лингвистический смысл, обычно она зависит от требований приложения.

Несколько примеров нормализации:

- Лингвистическая нормализация — словари Ispell пытаются свести слова на входе к нормализованной форме, а стеммеры убирают окончания слов

- Адреса URL могут быть канонизированы, чтобы например следующие адреса считались одинаковыми:

    - http://www.pgsql.ru/db/mw/index.html

    - http://www.pgsql.ru/db/mw/

    - http://www.pgsql.ru/db/../db/mw/index.html

- Названия цветов могут быть заменены их шестнадцатеричными значениями, например red, green, blue, magenta -> FF0000, 00FF00, 0000FF, FF00FF

- При индексировании чисел можно отбросить цифры в дробной части для сокращения множества всевозможных чисел, чтобы например 3.14159265359, 3.1415926 и 3.14 стали одинаковыми после нормализации, при которой после точки останутся только две цифры.

Словарь — это программа, которая принимает на вход фрагмент и возвращает:

- массив лексем, если входной фрагмент известен в словаре (заметьте, один фрагмент может породить несколько лексем)

- одну лексему с установленным флагом TSL_FILTER для замены исходного фрагмента новым, чтобы следующие словари работали с новым вариантом (словарь, который делает это, называется фильтрующим словарём)

- пустой массив, если словарь воспринимает этот фрагмент, но считает его стоп-словом

- NULL, если словарь не воспринимает полученный фрагмент

В Postgres Pro встроены стандартные словари для многих языков. Есть также несколько предопределённых шаблонов, на основании которых можно создавать новые словари с изменёнными параметрами. Все эти шаблоны описаны ниже. Если же ни один из них не подходит, можно создать и свои собственные шаблоны. Соответствующие примеры можно найти в каталоге contrib/ инсталляции Postgres Pro.

Конфигурация текстового поиска связывает анализатор с набором словарей, которые будут обрабатывать выделенные им фрагменты. Для каждого типа фрагментов, выданных анализатором, в конфигурации задаётся отдельный список словарей. Найденный анализатором фрагмент проходит через все словари по порядку, пока какой-либо словарь не увидит в нём знакомое для него слово. Если он окажется стоп-словом или его не распознает ни один словарь, этот фрагмент не будет учитываться при индексации и поиске. Обычно результат определяет первый же словарь, который возвращает не NULL, и остальные словари уже не проверяются; однако фильтрующий словарь может заменить полученное слово другим, которое и будет передано следующим словарям.

Общее правило настройки списка словарей заключается в том, чтобы поставить наиболее частные и специфические словари в начале, затем перечислить более общие и закончить самым общим словарём, например стеммером Snowball или словарём simple, который распознаёт всё. Например, для поиска по теме астрономии (конфигурация astro_en) тип фрагментов asciiword (слово из букв ASCII) можно связать со словарём синонимов астрономических терминов, затем с обычным английским словарём и наконец со стеммером английских окончаний Snowball:

    ALTER TEXT SEARCH CONFIGURATION astro_en
        ADD MAPPING FOR asciiword WITH astrosyn, english_ispell, english_stem;
Фильтрующий словарь можно включить в любом месте списка, кроме конца, где он будет бесполезен. Фильтрующие словари бывают полезны для частичной нормализации слов и упрощения задачи следующих словарей. Например, фильтрующий словарь может удалить из текста диакритические знаки, как это делает модуль unaccent.

### 12.6.1. Стоп-слова
Стоп-словами называются слова, которые встречаются очень часто, практически в каждом документе, и поэтому не имеют различительной ценности. Таким образом, при полнотекстовом поиске их можно игнорировать. Например, в каждом английском тексте содержатся артикли a и the, так что хранить их в индексе бессмысленно. Однако стоп-слова влияют на позиции лексем в значении tsvector, от чего, в свою очередь, зависит ранжирование:

    SELECT to_tsvector('english','in the list of stop words');
            to_tsvector
    ----------------------------
    'list':3 'stop':5 'word':6
В результате отсутствуют позиции 1,2,4, потому что фрагменты в этих позициях оказались стоп-словами. Ранги, вычисленные для документов со стоп-словами и без них, могут значительно различаться:

    SELECT ts_rank_cd (to_tsvector('english','in the list of stop words'),
    to_tsquery('list & stop'));
    ts_rank_cd
    ------------
        0.05

    SELECT ts_rank_cd (to_tsvector('english','list stop words'),
    to_tsquery('list & stop'));
    ts_rank_cd
    ------------
            0.1
Как именно обрабатывать стоп-слова, определяет сам словарь. Например, словари ispell сначала нормализуют слова, а затем просматривают список стоп-слов, тогда как стеммеры Snowball просматривают свой список стоп-слов в первую очередь. Это различие в поведении объясняется стремлением уменьшить шум.

### 12.6.2. Простой словарь
Работа шаблона словарей simple сводится к преобразованию входного фрагмента в нижний регистр и проверки результата по файлу со списком стоп-слов. Если это слово находится в файле, словарь возвращает пустой массив и фрагмент исключается из дальнейшего рассмотрения. В противном случае словарь возвращает в качестве нормализованной лексемы слово в нижнем регистре. Этот словарь можно настроить и так, чтобы все слова, кроме стоп-слов, считались неопознанными и передавались следующему словарю в списке.

Определить словарь на основе шаблона simple можно так:

    CREATE TEXT SEARCH DICTIONARY public.simple_dict (
        TEMPLATE = pg_catalog.simple,
        STOPWORDS = english
    );
Здесь english — базовое имя файла со стоп-словами. Полным именем файла будет $SHAREDIR/tsearch_data/english.stop, где $SHAREDIR указывает на каталог с общими данными Postgres Pro, часто это /usr/local/share/postgresql (точно узнать его можно с помощью команды pg_config --sharedir). Этот текстовый файл должен содержать просто список слов, по одному слову в строке. Пустые строки и окружающие пробелы игнорируются, все символы переводятся в нижний регистр и на этом обработка файла заканчивается.

Теперь мы можем проверить наш словарь:

    SELECT ts_lexize('public.simple_dict','YeS');
    ts_lexize
    -----------
    {yes}

    SELECT ts_lexize('public.simple_dict','The');
    ts_lexize
    -----------
    {}
Мы также можем настроить словарь так, чтобы он возвращал NULL вместо слова в нижнем регистре, если оно не находится в файле стоп-слов. Для этого нужно присвоить параметру Accept значение false. Продолжая наш пример:

    ALTER TEXT SEARCH DICTIONARY public.simple_dict ( Accept = false );

    SELECT ts_lexize('public.simple_dict','YeS');
    ts_lexize
    -----------


    SELECT ts_lexize('public.simple_dict','The');
    ts_lexize
    -----------
    {}
Со значением Accept = true (по умолчанию) словарь simple имеет смысл включать только в конце списка словарей, так как он никогда не передаст фрагмент следующему словарю. И напротив, Accept = false имеет смысл, только если за ним следует ещё минимум один словарь.

> Внимание
Большинство словарей работают с дополнительными файлами, например, файлами стоп-слов. Содержимое этих файлов должно иметь кодировку UTF-8. Если база данных работает в другой кодировке, они будут переведены в неё, когда сервер будет загружать их.

> Внимание
Обычно в рамках одного сеанса дополнительный файл словаря загружается только один раз, при первом использовании. Если же вы измените его и захотите, чтобы существующие сеансы работали с новым содержимым, выполните для этого словаря команду ALTER TEXT SEARCH DICTIONARY. Это обновление словаря может быть «фиктивным», фактически не меняющим значения никаких параметров.

### 12.6.3. Словарь синонимов
Этот шаблон словарей используется для создания словарей, заменяющих слова синонимами. Словосочетания такие словари не поддерживают (используйте для этого тезаурус (Подраздел 12.6.4)). Словарь синонимов может помочь в преодолении лингвистических проблем, например, не дать стеммеру английского уменьшить слово «Paris» до «pari». Для этого достаточно поместить в словарь синонимов строку Paris paris и поставить этот словарь перед словарём english_stem. Например:

    SELECT * FROM ts_debug('english', 'Paris');
    alias  |   description  | token|  dictionaries |  dictionary | lexemes
    ----------+----------------+------+---------------+-------------+--------
    asciiword| Word, all ASCII| Paris| {english_stem}| english_stem| {pari}

    CREATE TEXT SEARCH DICTIONARY my_synonym (
        TEMPLATE = synonym,
        SYNONYMS = my_synonyms
    );

    ALTER TEXT SEARCH CONFIGURATION english
        ALTER MAPPING FOR asciiword
        WITH my_synonym, english_stem;

    SELECT * FROM ts_debug('english', 'Paris');
    alias  |   description  | token| dictionaries | dictionary| lexemes
    ----------+----------------+------+--------------+-----------+--------
    asciiword| Word, all ASCII| Paris| {my_synonym, | my_synonym| {paris}
            |                |      | english_stem}|           |
Шаблон synonym принимает единственный параметр, SYNONYMS, в котором задаётся базовое имя его файла конфигурации — в данном примере это my_synonyms. Полным именем файла будет $SHAREDIR/tsearch_data/my_synonyms.syn (где $SHAREDIR указывает на каталог общих данных Postgres Pro). Содержимое этого файла должны составлять строки с двумя словами в каждой (первое — заменяемое слово, а второе — его синоним), разделёнными пробелами. Пустые строки и окружающие пробелы при разборе этого файла игнорируются.

Шаблон synonym также принимает необязательный параметр CaseSensitive, который по умолчанию имеет значение false. Когда CaseSensitive равен false, слова в файле синонимов переводятся в нижний регистр, вместе с проверяемыми фрагментами. Если же он не равен true, регистр слов в файле и проверяемых фрагментов не меняются, они сравниваются «как есть».

В конце синонима в этом файле можно добавить звёздочку (*), тогда этот синоним будет рассматриваться как префикс. Эта звёздочка будет игнорироваться в to_tsvector(), но to_tsquery() изменит результат, добавив в него маркер сопоставления префикса (см. Подраздел 12.3.2). Например, предположим, что файл $SHAREDIR/tsearch_data/synonym_sample.syn имеет следующее содержание:

    postgres        pgsql
    postgresql      pgsql
    postgre pgsql
    gogle   googl
    indices index*
С ним мы получим такие результаты:

    mydb=# CREATE TEXT SEARCH DICTIONARY
    syn (template=synonym, synonyms='synonym_sample');
    mydb=# SELECT ts_lexize('syn','indices');
    ts_lexize
    -----------
    {index}
    (1 row)

    mydb=# CREATE TEXT SEARCH CONFIGURATION tst (copy=simple);
    mydb=# ALTER TEXT SEARCH CONFIGURATION tst ALTER MAPPING FOR asciiword
    WITH syn;
    mydb=# SELECT to_tsvector('tst','indices');
    to_tsvector
    -------------
    'index':1
    (1 row)

    mydb=# SELECT to_tsquery('tst','indices');
    to_tsquery
    ------------
    'index':*
    (1 row)

    mydb=# SELECT 'indexes are very useful'::tsvector;
                tsvector             
    ---------------------------------
    'are' 'indexes' 'useful' 'very'
    (1 row)

    mydb=# SELECT 'indexes are very useful'::tsvector @@
    to_tsquery('tst','indices');
    ?column?
    ----------
    t
    (1 row)
### 12.6.4. Тезаурус
Тезаурус (или сокращённо TZ) содержит набор слов и информацию о связях слов и словосочетаний, то есть более широкие понятия (Broader Terms, BT), более узкие понятия (Narrow Terms, NT), предпочитаемые названия, исключаемые названия, связанные понятия и т. д.

В основном тезаурус заменяет исключаемые слова и словосочетания предпочитаемыми и может также сохранить исходные слова для индексации. Текущая реализация тезауруса в Postgres Pro представляет собой расширение словаря синонимов с поддержкой фраз. Конфигурация тезауруса определяется файлом следующего формата:

    # это комментарий
    образец слов(а) : индексируемые слова
    другой образец слов(а) : другие индексируемые слова
    ...
Здесь двоеточие (:) служит разделителем между исходной фразой и её заменой.

Прежде чем проверять соответствие фраз, тезаурус нормализует файл конфигурации, используя внутренний словарь (который указывается в конфигурации словаря-тезауруса). Этот внутренний словарь для тезауруса может быть только одним. Если он не сможет распознать какое-либо слово, произойдёт ошибка. В этом случае необходимо либо исключить это слово, либо добавить его во внутренний словарь. Также можно добавить звёздочку (*) перед индексируемыми словами, чтобы они не проверялись по внутреннему словарю, но все слова-образцы должны быть известны внутреннему словарю.

- Если входному фрагменту соответствуют несколько фраз в этом списке, тезаурус выберет самое длинное определение, а если таких окажется несколько, самое последнее из них.

- Выделить во фразе какие-то стоп-слова нельзя; вместо этого можно вставить ? в том месте, где может оказаться стоп-слово. Например, в предположении, что a и the — стоп-слова по внутреннему словарю:

    ? one ? two : swsw
соответствует входным строкам a one the two и the one a two, так что обе эти строки будут заменены на swsw.

Как и обычный словарь, тезаурус должен привязываться к лексемам определённых типов. Так как тезаурус может распознавать фразы, он должен запоминать своё состояние и взаимодействовать с анализатором. Учитывая свои привязки, он может либо обрабатывать следующий фрагмент, либо прекратить накопление фразы. Поэтому настройка тезаурусов в системе требует особого внимания. Например, если привязать тезаурус только к типу фрагментов asciiword, тогда определение в тезаурусе one 7 не будет работать, так как этот тезаурус не связан с типом uint.

> Внимание
Тезаурусы используются при индексации, поэтому при любом изменении параметров или содержимого тезауруса необходима переиндексация. Для большинства других типов словарей при небольших изменениях, таких как удаление и добавление стоп-слов, переиндексация не требуется.

#### 12.6.4.1. Конфигурация тезауруса
Для создания нового словаря-тезауруса используется шаблон thesaurus. Например:

    CREATE TEXT SEARCH DICTIONARY thesaurus_simple (
        TEMPLATE = thesaurus,
        DictFile = mythesaurus,
        Dictionary = pg_catalog.english_stem
    );
Здесь:

    thesaurus_simple — имя нового словаря

mythesaurus — базовое имя файла конфигурации тезауруса. (Полным путём к файлу будет $SHAREDIR/tsearch_data/mythesaurus.ths, где $SHAREDIR указывает на каталог общих данных PostgreSQL.)

pg_catalog.english_stem — внутренний словарь (в данном случае это стеммер Snowball для английского) для нормализации тезауруса. Заметьте, что внутренний словарь имеет собственную конфигурацию (например, список стоп-слов), но здесь она не рассматривается.

Теперь тезаурус thesaurus_simple можно связать с желаемыми типами фрагментов в конфигурации, например так:

    ALTER TEXT SEARCH CONFIGURATION english
        ALTER MAPPING FOR asciiword, asciihword, hword_asciipart
        WITH thesaurus_simple;
#### 12.6.4.2. Пример тезауруса
Давайте рассмотрим простой астрономический тезаурус thesaurus_astro, содержащий несколько астрономических терминов:

    supernovae stars : sn
    crab nebulae : crab
Ниже мы создадим словарь и привяжем некоторые типы фрагментов к астрономическому тезаурусу и английскому стеммеру:

    CREATE TEXT SEARCH DICTIONARY thesaurus_astro (
        TEMPLATE = thesaurus,
        DictFile = thesaurus_astro,
        Dictionary = english_stem
    );

    ALTER TEXT SEARCH CONFIGURATION russian
        ALTER MAPPING FOR asciiword, asciihword, hword_asciipart
        WITH thesaurus_astro, english_stem;
Теперь можно проверить, как он работает. Функция ts_lexize не очень полезна для проверки тезауруса, так как она обрабатывает входную строку как один фрагмент. Вместо неё мы можем использовать функции plainto_tsquery и to_tsvector, которые разбивают входную строку на несколько фрагментов:

    SELECT plainto_tsquery('supernova star');
    plainto_tsquery
    -----------------
    'sn'

    SELECT to_tsvector('supernova star');
    to_tsvector
    -------------
    'sn':1
В принципе так же можно использовать to_tsquery, если заключить аргумент в кавычки:

    SELECT to_tsquery(' ''supernova star''');
    to_tsquery
    ------------
    'sn'
Заметьте, что supernova star совпадает с supernovae stars в thesaurus_astro, так как мы подключили стеммер english_stem в определении тезауруса. Этот стеммер удалил конечные буквы e и s.

Чтобы проиндексировать исходную фразу вместе с заменой, её нужно просто добавить в правую часть соответствующего определения:

    supernovae stars : sn supernovae stars

    SELECT plainto_tsquery('supernova star');
        plainto_tsquery
    -----------------------------
    'sn' & 'supernova' & 'star'
### 12.6.5. Словарь Ispell
Шаблон словарей Ispell поддерживает морфологические словари, которые могут сводить множество разных лингвистических форм слова к одной лексеме. Например, английский словарь Ispell может связать вместе все склонения и спряжения ключевого слова bank: banking, banked, banks, banks',bank's и т. п.

Стандартный дистрибутив Postgres Pro не включает файлы конфигурации Ispell. Загрузить словари для множества языков можно со страницы Ispell. Кроме того, поддерживаются и другие современные форматы словарей: MySpell (OO < 2.0.1) и Hunspell (OO >= 2.0.2). Большой набор соответствующих словарей можно найти на странице OpenOffice Wiki.

Чтобы создать словарь Ispell, выполните следующие действия:

- загрузите файлы конфигурации словаря. Пакет с дополнительным словарём OpenOffice имеет расширение .oxt. Из него необходимо извлечь файлы .aff и .dic, и сменить их расширения на .affix и .dict, соответственно. Для некоторых файлов словарей также необходимо преобразовать символы в кодировку UTF-8 с помощью, например, таких команд (для норвежского языка):

    iconv -f ISO_8859-1 -t UTF-8 -o nn_no.affix nn_NO.aff
    iconv -f ISO_8859-1 -t UTF-8 -o nn_no.dict nn_NO.dic
- скопируйте файлы в каталог $SHAREDIR/tsearch_data

- загрузите эти файлы в Postgres Pro следующей командой:

    CREATE TEXT SEARCH DICTIONARY english_hunspell (
        TEMPLATE = ispell,
        DictFile = en_us,
        AffFile = en_us,
        Stopwords = english);
Здесь параметры DictFile, AffFile и StopWords определяют базовые имена файлов словаря, аффиксов и стоп-слов. Файл стоп-слов должен иметь тот же формат, что рассматривался выше в описании словаря simple. Формат других файлов здесь не рассматривается, но его можно узнать по вышеуказанным веб-адресам.

Словари Ispell обычно воспринимают ограниченный набор слов, так что за ними следует подключить другой, более общий словарь, например, Snowball, который принимает всё.

Файл .affix для Ispell имеет такую структуру:

    prefixes
    flag *A:
        .           >   RE      # As in enter > reenter
    suffixes
    flag T:
        E           >   ST      # As in late > latest
        [^AEIOU]Y   >   -Y,IEST # As in dirty > dirtiest
        [AEIOU]Y    >   EST     # As in gray > grayest
        [^EY]       >   EST     # As in small > smallest
А файл .dict — такую:

    lapse/ADGRS
    lard/DGRS
    large/PRTY
    lark/MRS
Формат файла .dict следующий:

    basic_form/affix_class_name
В файле .affix каждый флаг аффиксов описывается в следующем формате:

    условие > [-отсекаемые_буквы,] добавляемый_аффикс
Здесь условие записывается в формате, подобном формату регулярных выражений. В нём возможно описать группы [...] и [^...]. Например, запись [AEIOU]Y означает, что последняя буква слова — "y", а предпоследней может быть "a", "e", "i", "o" или "u". Запись [^EY] означает, что последняя буква не "e" и не "y".

Словари Ispell поддерживают разделение составных слов, что бывает полезно. Заметьте, что для этого в файле аффиксов нужно пометить специальным оператором compoundwords controlled слова, которые могут участвовать в составных образованиях:

    compoundwords  controlled z
Вот как это работает для норвежского языка:

    SELECT ts_lexize('norwegian_ispell',
    'overbuljongterningpakkmesterassistent');
    {over,buljong,terning,pakk,mester,assistent}
    SELECT ts_lexize('norwegian_ispell', 'sjokoladefabrikk');
    {sjokoladefabrikk,sjokolade,fabrikk}
Формат MySpell представляет собой подмножество формата Hunspell. Файл .affix словаря Hunspell имеет следующую структуру:

    PFX A Y 1
    PFX A   0     re         .
    SFX T N 4
    SFX T   0     st         e
    SFX T   y     iest       [^aeiou]y
    SFX T   0     est        [aeiou]y
    SFX T   0     est        [^ey]
Первая строка класса аффиксов — заголовок. Поля правил аффиксов указываются после заголовка:

- имя параметра (PFX или SFX)

- флаг (имя класса аффиксов)

- отсекаемые символы в начале (в префиксе) или в конце (в суффиксе) слова

- добавляемый аффикс

- условие в формате, подобном регулярным выражениям.

Файл .dict подобен файлу .dict словаря Ispell:

    larder/M
    lardy/RT
    large/RSPMYT
    largehearted
> Примечание
Словарь MySpell не поддерживает составные слова. С другой стороны, Hunspell поддерживает множество операции с ними, но в настоящее время Postgres Pro использует только самые простые из этого множества.

### 12.6.6. Словарь Snowball
Шаблон словарей Snowball основан на проекте Мартина Потера, изобретателя популярного алгоритма стемминга для английского языка. Сейчас Snowball предлагает алгоритмы и для многих других языков (за подробностями обратитесь на сайт Snowball). Каждый алгоритм знает, как для данного языка свести распространённые словоформы к начальной форме. Для словаря Snowball задаётся обязательный параметр language, определяющий, какой именно стеммер использовать, и может задаваться параметр stopword, указывающий файл со списком исключаемых слов. (Стандартные списки стоп-слов Postgres Pro используется также в и проекте Snowball.) Например, встроенное определение выглядит так

    CREATE TEXT SEARCH DICTIONARY english_stem (
        TEMPLATE = snowball,
        Language = english,
        StopWords = english
    );
Формат файла стоп-слов не отличается от рассмотренного ранее.

Словарь Snowball распознаёт любые фрагменты, даже если он не может упростить слова, так что он должен быть самым последним в списке словарей. Помещать его перед другими словарями нет смысла, так как после него никакой фрагмент не будет передан следующему словарю.

## 12.7. Пример конфигурации
Конфигурация текстового поиска определяет всё, что необходимо для преобразования документа в формат tsvector: анализатор, который будет разбивать текст на фрагменты, и словари, которые будут преобразовывать фрагменты в лексемы. При каждом вызове to_tsvector или to_tsquery обязательно используется конфигурация текстового поиска. В конфигурации сервера есть параметр default_text_search_config, задающий имя конфигурации текстового поиска по умолчанию, которая будет использоваться, когда при вызове функций поиска соответствующий аргумент не определён. Этот параметр можно задать в postgresql.conf или установить в рамках отдельного сеанса с помощью команды SET.

В системе есть несколько встроенных конфигураций текстового поиска и вы можете легко дополнить их своими. Для удобства управления объектами текстового поиска в PostgreSQL реализованы соответствующие SQL-команды и специальные команды в psql, выводящие информацию об этих объектах (Раздел 12.10).

В качестве примера использования этих команд мы создадим конфигурацию pg, взяв за основу встроенную конфигурацию english:

    CREATE TEXT SEARCH CONFIGURATION public.pg ( COPY = pg_catalog.english );
Мы будем использовать список синонимов, связанных с Postgres Pro, в файле $SHAREDIR/tsearch_data/pg_dict.syn. Этот файл содержит строки:

    postgres    pg
    pgsql       pg
    postgresql  pg
Мы определим словарь синонимов следующим образом:

    CREATE TEXT SEARCH DICTIONARY pg_dict (
        TEMPLATE = synonym,
        SYNONYMS = pg_dict
    );
Затем мы зарегистрируем словарь Ispell english_ispell, у которого есть собственные файлы конфигурации:

    CREATE TEXT SEARCH DICTIONARY english_ispell (
        TEMPLATE = ispell,
        DictFile = english,
        AffFile = english,
        StopWords = english
    );
Теперь мы можем настроить сопоставления для слов в конфигурации pg:

    ALTER TEXT SEARCH CONFIGURATION pg
        ALTER MAPPING FOR asciiword, asciihword, hword_asciipart,
                        word, hword, hword_part
        WITH pg_dict, english_ispell, english_stem;
Мы решили не индексировать и не учитывать при поиске некоторые типы фрагментов, которые не обрабатываются встроенной конфигурацией:

    ALTER TEXT SEARCH CONFIGURATION pg
        DROP MAPPING FOR email, url, url_path, sfloat, float;
Теперь мы можем протестировать нашу конфигурацию:

    SELECT * FROM ts_debug('public.pg', '
    Postgres Pro, the highly scalable, SQL compliant, open source
    object-relational database management system, is now undergoing
    beta testing of the next version of our software.
    ');
И наконец мы выбираем в текущем сеансе эту конфигурацию, созданную в схеме public:

    => \dF
    List of text search configurations
    Schema  | Name | Description
    ---------+------+-------------
    public  | pg   |

    SET default_text_search_config = 'public.pg';
    SET

    SHOW default_text_search_config;
    default_text_search_config
    ----------------------------
    public.pg

## 12.8. Тестирование и отладка текстового поиска
12.8.1. Тестирование конфигурации
12.8.2. Тестирование анализатора
12.8.3. Тестирование словаря
Поведение нестандартной конфигурации текстового поиска по мере её усложнения может стать непонятным. В этом разделе описаны функции, полезные для тестирования объектов текстового поиска. Вы можете тестировать конфигурацию как целиком, так и по частям, отлаживая анализаторы и словари по отдельности.

### 12.8.1. Тестирование конфигурации
Созданную конфигурацию текстового поиска можно легко протестировать с помощью функции ts_debug.

    ts_debug([конфигурация regconfig,] документ text,
            OUT псевдоним text,
            OUT описание text,
            OUT фрагмент text,
            OUT словари regdictionary[],
            OUT словарь regdictionary,
            OUT лексемы text[])
            returns setof record
- ts_debug выводит информацию обо всех фрагментах данного документа, которые были выданы анализатором и обработаны настроенными словарями. Она использует конфигурацию, указанную в аргументе config, или default_text_search_config, если этот аргумент опущен.

- ts_debug возвращает по одной строке для каждого фрагмента, найденного в тексте анализатором. Эта строка содержит следующие столбцы:

- синоним text — краткое имя типа фрагмента

- описание text — описание типа фрагмента

- фрагмент text — текст фрагмента

- словари regdictionary[] — словари, назначенные в конфигурации для фрагментов такого типа

- словарь regdictionary — словарь, распознавший этот фрагмент, или NULL, если подходящего словаря не нашлось

- лексемы text[] — лексемы, выданные словарём, распознавшим фрагмент, или NULL, если подходящий словарь не нашёлся; может быть также пустым массивом ({}), если фрагмент распознан как стоп-слово

Простой пример:

    SELECT * FROM ts_debug('english',
    'a fat  cat sat on a mat - it ate a fat rats');
    alias  |   description  | token|  dictionaries |  dictionary |lexemes
    ----------+----------------+------+---------------+-------------+-------
    asciiword| Word, all ASCII| a    | {english_stem}| english_stem| {}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| fat  | {english_stem}| english_stem| {fat}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| cat  | {english_stem}| english_stem| {cat}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| sat  | {english_stem}| english_stem| {sat}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| on   | {english_stem}| english_stem| {}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| a    | {english_stem}| english_stem| {}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| mat  | {english_stem}| english_stem| {mat}
    blank    | Space symbols  |      | {}            |             | 
    blank    | Space symbols  | -    | {}            |             | 
    asciiword| Word, all ASCII| it   | {english_stem}| english_stem| {}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| ate  | {english_stem}| english_stem| {ate}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| a    | {english_stem}| english_stem| {}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| fat  | {english_stem}| english_stem| {fat}
    blank    | Space symbols  |      | {}            |             | 
    asciiword| Word, all ASCII| rats | {english_stem}| english_stem| {rat}

Для более полной демонстрации мы сначала создадим конфигурацию public.english и словарь Ispell для английского языка:

    CREATE TEXT SEARCH CONFIGURATION public.english
    ( COPY = pg_catalog.english );

    CREATE TEXT SEARCH DICTIONARY english_ispell (
        TEMPLATE = ispell,
        DictFile = english,
        AffFile = english,
        StopWords = english
    );

    ALTER TEXT SEARCH CONFIGURATION public.english
    ALTER MAPPING FOR asciiword WITH english_ispell, english_stem;
    SELECT * FROM ts_debug('public.english','The Brightest supernovaes');
    alias  | description |   token   |  dictionaries |dictionary| lexemes   
    ---------+-------------+-----------+----------- ---+----------+-----------
    asciiword|Word,        |The        |{english_ispell|english_  |{}
            | all ASCII   |           |,english_stem} |ispell    |
    blank    |Space symbols|           |{}             |          |
            |             |           |               |          |
    asciiword|Word,        |Brightest  |{english_ispell|english_  |{bright}
            |all ASCII    |           |,english_stem} |ispell    |
    blank    |Space symbols|           | {}            |          |
            |             |           |               |          |
    asciiword|Word,        |supernovaes|{english_ispell|english_  |{supernova}
            |all ASCII    |           |,english_stem} |stem      |
В этом примере слово Brightest было воспринято анализатором как фрагмент ASCII word (синоним asciiword). Для этого типа фрагментов список словарей включает english_ispell и english_stem. Данное слово было распознано словарём english_ispell, который свёл его к bright. Слово supernovaes оказалось незнакомо словарю english_ispell, так что оно было передано следующему словарю, который его благополучно распознал (на самом деле english_stem — это стеммер Snowball, который распознаёт всё, поэтому он включён в список словарей последним).

Слово The было распознано словарём english_ispell как стоп-слово (см. Подраздел 12.6.1) и поэтому не будет индексироваться. Пробелы тоже отбрасываются, так как в данной конфигурации для них нет словарей.

Вы можете уменьшить ширину вывода, явно перечислив только те столбцы, которые вы хотите видеть:

    SELECT alias, token, dictionary, lexemes
    FROM ts_debug('public.english','The Brightest supernovaes');
    alias   |    token    |   dictionary   |   lexemes   
    -----------+-------------+----------------+-------------
    asciiword | The         | english_ispell | {}
    blank     |             |                | 
    asciiword | Brightest   | english_ispell | {bright}
    blank     |             |                | 
    asciiword | supernovaes | english_stem   | {supernova}
### 12.8.2. Тестирование анализатора
Следующие функции позволяют непосредственно протестировать анализатор текстового поиска.

    ts_parse(имя_анализатора text, документ text,
            OUT код_фрагмента integer, OUT фрагмент text) returns setof record
    ts_parse(oid_анализатора oid, документ text,
            OUT код_фрагмента integer, OUT фрагмент text) returns setof record
ts_parse разбирает данный документ и возвращает набор записей, по одной для каждого извлечённого фрагмента. Каждая запись содержит код_фрагмента, код назначенного типа фрагмента, и фрагмент, собственно текст фрагмента. Например:

    SELECT * FROM ts_parse('default', '123 - a number');
    tokid | token
    -------+--------
        22 | 123
        12 |
        12 | -
        1 | a
        12 |
        1 | number
    ts_token_type(имя_анализатора text, OUT код_фрагмента integer,
                OUT псевдоним text, OUT описание text) returns setof record
    ts_token_type(oid_анализатора oid, OUT код_фрагмента integer,
                OUT псевдоним text, OUT описание text) returns setof record
ts_token_type возвращает таблицу, описывающую все типы фрагментов, которые может распознать анализатор. Для каждого типа в этой таблице указывается его целочисленный код_фрагмента, псевдоним , с которым этот тип фигурирует в командах, и краткое description. Например:

    SELECT * FROM ts_token_type('default');
    tokid |      alias      |               description                
    -------+-----------------+------------------------------------------
        1 | asciiword       | Word, all ASCII
        2 | word            | Word, all letters
        3 | numword         | Word, letters and digits
        4 | email           | Email address
        5 | url             | URL
        6 | host            | Host
        7 | sfloat          | Scientific notation
        8 | version         | Version number
        9 | hword_numpart   | Hyphenated word part, letters and digits
        10 | hword_part      | Hyphenated word part, all letters
        11 | hword_asciipart | Hyphenated word part, all ASCII
        12 | blank           | Space symbols
        13 | tag             | XML tag
        14 | protocol        | Protocol head
        15 | numhword        | Hyphenated word, letters and digits
        16 | asciihword      | Hyphenated word, all ASCII
        17 | hword           | Hyphenated word, all letters
        18 | url_path        | URL path
        19 | file            | File or path name
        20 | float           | Decimal notation
        21 | int             | Signed integer
        22 | uint            | Unsigned integer
        23 | entity          | XML entity
### 12.8.3. Тестирование словаря
Для тестирования словаря предназначена функция ts_lexize.

    ts_lexize(словарь regdictionary, фрагмент text) returns text[]
ts_lexize возвращает массив лексем, если входной фрагмент известен словарю, либо пустой массив, если этот фрагмент считается в словаре стоп-словом, либо NULL, если он не был распознан.

Примеры:

    SELECT ts_lexize('english_stem', 'stars');
    ts_lexize
    -----------
    {star}

    SELECT ts_lexize('english_stem', 'a');
    ts_lexize
    -----------
    {}
> Примечание
Функция ts_lexize принимает одиночный фрагмент, а не просто текст. Вот пример возможного заблуждения:

    SELECT ts_lexize('thesaurus_astro','supernovae stars') is null;
    ?column?
    ----------
    t
Хотя фраза supernovae stars есть в тезаурусе thesaurus_astro, ts_lexize не работает, так как она не разбирает входной текст, а воспринимает его как один фрагмент. Поэтому для проверки тезаурусов следует использовать функции plainto_tsquery и to_tsvector, например:

    SELECT plainto_tsquery('supernovae stars');
    plainto_tsquery
    -----------------
    'sn'    

## 12.9. Типы индексов GIN и GiST
Для ускорения полнотекстового поиска можно использовать индексы двух видов. Заметьте, что эти индексы не требуются для поиска, но если по какому-то столбцу поиск выполняется регулярно, обычно желательно её индексировать.

    CREATE INDEX имя ON таблица USING GIN (столбец);
Создаёт индекс на базе GIN (Generalized Inverted Index, Обобщённый Инвертированный Индекс). Столбец должен иметь тип tsvector.

    CREATE INDEX имя ON таблица USING GIST (столбец);
Создаёт индекс на базе GiST (Generalized Search Tree, Обобщённое дерево поиска). Здесь столбец может иметь тип tsvector или tsquery.

Более предпочтительными для текстового поиска являются индексы GIN. Будучи инвертированными индексами, они содержат записи для всех отдельных слов (лексем) с компактным списком мест их вхождений. При поиске нескольких слов можно найти первое, а затем воспользоваться индексом и исключить строки, в которых дополнительные слова отсутствуют. Индексы GIN хранят только слова (лексемы) из значений tsvector, и теряют информацию об их весах. Таким образом для выполнения запроса с весами потребуется перепроверить строки в таблице.

Индекс GiST допускает неточности, то есть он допускает ложные попадания и поэтому их нужно исключать дополнительно, сверяя результат с фактическими данными таблицы. (Postgres Pro делает это автоматически.) Индексы GiST являются неточными, так как все документы в них представляются сигнатурой фиксированной длины. Эта сигнатура создаётся в результате представления присутствия каждого слова как одного бита в строке из n-бит, а затем логического объединения этих битовых строк. Если двум словам будет соответствовать одна битовая позиция, попадание оказывается ложным. Если для всех слов оказались установлены соответствующие биты (в случае фактического или ложного попадания), для проверки правильности предположения о совпадении слов необходимо прочитать строку таблицы.

Индекс GiST может быть покрывающим, то есть использовать функциональность INCLUDE. В качестве дополнительных в него могут включаться столбцы, для типа данных которых не определён класс операторов GiST. Атрибуты, включаемые в индекс, сохраняются в нём без сжатия.

Неточность индекса приводит к снижению производительности из-за дополнительных обращений к записям таблицы, для которых предположение о совпадении оказывается ложным. Так как произвольный доступ к таблице обычно не бывает быстрым, это ограничивает применимость индексов GiST. Вероятность ложных попаданий зависит от ряда факторов, например от количества уникальных слов, так что его рекомендуется сокращать, применяя словари.

Заметьте, что построение индекса GIN часто можно ускорить, увеличив maintenance_work_mem, тогда как время построения индекса GiST не зависит от этого параметра.

Правильно используя индексы GIN и GiST и разделяя большие коллекции документов на секции, можно реализовать очень быстрый поиск с возможностью обновления «на лету». Секционировать данные можно как на уровне базы, с использованием наследования таблиц, так и распределив документы по разным серверам и затем собирая внешние результаты, например, средствами доступа к сторонним данным. Последний вариант возможен благодаря тому, что функции ранжирования используют только локальную информацию. 

## 12.10. Поддержка psql
Информацию об объектах конфигурации текстового поиска можно получить в psql с помощью следующего набора команд:

#### \dF{d,p,t}[+] [ШАБЛОН]
Необязательный + в этих командах включает более подробный вывод.

В необязательном параметре ШАБЛОН может указываться имя объекта текстового поиска (возможно, дополненное схемой). Если ШАБЛОН не указан, выводится информация обо всех видимых объектах. ШАБЛОН может содержать регулярное выражение с разными масками для схемы и объекта. Это иллюстрируют следующие примеры:

    => \dF *fulltext*
        List of text search configurations
    Schema |  Name        | Description
    --------+--------------+-------------
    public  | fulltext_cfg |
    => \dF *.fulltext*
        List of text search configurations
    Schema   |  Name        | Description
    ----------+----------------------------
    fulltext | fulltext_cfg |
    public   | fulltext_cfg |
Возможны следующие команды:

#### \dF[+] [ШАБЛОН]
Список конфигураций текстового поиска (добавьте + для дополнительных сведений).

    => \dF russian
                List of text search configurations
    Schema   |  Name   |            Description             
    ------------+---------+------------------------------------
    pg_catalog | russian | configuration for russian language

    => \dF+ russian
    Text search configuration "pg_catalog.russian"
    Parser: "pg_catalog.default"
        Token      | Dictionaries 
    -----------------+--------------
    asciihword      | english_stem
    asciiword       | english_stem
    email           | simple
    file            | simple
    float           | simple
    host            | simple
    hword           | russian_stem
    hword_asciipart | english_stem
    hword_numpart   | simple
    hword_part      | russian_stem
    int             | simple
    numhword        | simple
    numword         | simple
    sfloat          | simple
    uint            | simple
    url             | simple
    url_path        | simple
    version         | simple
    word            | russian_stem
#### \dFd[+] [ШАБЛОН]
Список словарей текстового поиска (добавьте + для дополнительных сведений).

    => \dFd
                                List of text search dictionaries
    Schema   |      Name       |                        Description
    ------------+-----------------+-----------------------------------------------------------
    pg_catalog | arabic_stem     | snowball stemmer for arabic language
    pg_catalog | danish_stem     | snowball stemmer for danish language
    pg_catalog | dutch_stem      | snowball stemmer for dutch language
    pg_catalog | english_stem    | snowball stemmer for english language
    pg_catalog | finnish_stem    | snowball stemmer for finnish language
    pg_catalog | french_stem     | snowball stemmer for french language
    pg_catalog | german_stem     | snowball stemmer for german language
    pg_catalog | hungarian_stem  | snowball stemmer for hungarian language
    pg_catalog | indonesian_stem | snowball stemmer for indonesian language
    pg_catalog | irish_stem      | snowball stemmer for irish language
    pg_catalog | italian_stem    | snowball stemmer for italian language
    pg_catalog | lithuanian_stem | snowball stemmer for lithuanian language
    pg_catalog | nepali_stem     | snowball stemmer for nepali language
    pg_catalog | norwegian_stem  | snowball stemmer for norwegian language
    pg_catalog | portuguese_stem | snowball stemmer for portuguese language
    pg_catalog | romanian_stem   | snowball stemmer for romanian language
    pg_catalog | russian_stem    | snowball stemmer for russian language
    pg_catalog | simple          | simple dictionary: just lower case and check for stopword
    pg_catalog | spanish_stem    | snowball stemmer for spanish language
    pg_catalog | swedish_stem    | snowball stemmer for swedish language
    pg_catalog | tamil_stem      | snowball stemmer for tamil language
    pg_catalog | turkish_stem    | snowball stemmer for turkish language
 #### \dFp[+] [ШАБЛОН]
Список анализаторов текстового поиска (добавьте + для дополнительных сведений).

    => \dFp
            List of text search parsers
    Schema   |  Name   |     Description     
    ------------+---------+---------------------
    pg_catalog | default | default word parser
    => \dFp+
        Text search parser "pg_catalog.default"
        Method      |    Function    | Description 
    -----------------+----------------+-------------
    Start parse     | prsd_start     | 
    Get next token  | prsd_nexttoken | 
    End parse       | prsd_end       | 
    Get headline    | prsd_headline  | 
    Get token types | prsd_lextype   | 

            Token types for parser "pg_catalog.default"
    Token name    |               Description                
    -----------------+------------------------------------------
    asciihword      | Hyphenated word, all ASCII
    asciiword       | Word, all ASCII
    blank           | Space symbols
    email           | Email address
    entity          | XML entity
    file            | File or path name
    float           | Decimal notation
    host            | Host
    hword           | Hyphenated word, all letters
    hword_asciipart | Hyphenated word part, all ASCII
    hword_numpart   | Hyphenated word part, letters and digits
    hword_part      | Hyphenated word part, all letters
    int             | Signed integer
    numhword        | Hyphenated word, letters and digits
    numword         | Word, letters and digits
    protocol        | Protocol head
    sfloat          | Scientific notation
    tag             | XML tag
    uint            | Unsigned integer
    url             | URL
    url_path        | URL path
    version         | Version number
    word            | Word, all letters
    (23 rows)
 #### \dFt[+] [ШАБЛОН]
Список шаблонов текстового поиска (добавьте + для дополнительных сведений).

    => \dFt
                            List of text search templates
    Schema  |  Name   |                       Description                  
    ----------+---------+----------------------------------------------------
    pg_catalog|ispell   |ispell dictionary
    pg_catalog|simple   |simple dictionary: just lower case and check for ...
    pg_catalog|snowball |snowball stemmer
    pg_catalog|synonym  |synonym dictionary: replace word by its synonym
    pg_catalog|thesaurus|thesaurus dictionary: phrase by phrase substitution

## 12.11. Ограничения
Текущая реализация текстового поиска в Postgres Pro имеет следующие ограничения:

- Длина лексемы не может превышать 2 килобайта

- Длина значения tsvector (лексемы и их позиции) не может превышать 1 мегабайт

- Число лексем должно быть меньше 264

- Значения позиций в tsvector должны быть от 0 до 16383

- Расстояние в операторе <N> (ПРЕДШЕСТВУЕТ) типа tsquery не может быть больше 16384

- Не больше 256 позиций для одной лексемы

- Число узлов (лексемы + операторы) в значении tsquery должно быть меньше 32768

Для сравнения, документация PostgreSQL 8.1 содержала 335 420 слов, из них 10 441 уникальных, а наиболее часто употребляющееся в ней слово «postgresql» встречается 6 127 раз в 655 документах.

Другой пример — архивы списков рассылки PostgreSQL содержали 910 989 уникальных слов в 57 491 343 лексемах в 461 020 сообщениях.